目前市场上越来越多的 Android App采用 C/C++ 来实现其关键逻辑,尤其是很多第三方的SDK,出于效率、安全,复用的考虑,比如人脸识别,语音识别等等。所以能分析 C/C++ 崩溃日志并能从日志中分析出原因,成为 Android 开发人员一项必备技能。本文将通过一个简单的Demo分析 Native 崩溃日志来定位出错的 C/C++ 代码及出错原因。
1、问题现场
为了方便,直接使用Android Studio中Sample:hello-jni,修改app/src/main/cpp/hello-jni.c
文件内容如下,手动造一个Native Crash问题,这样在App启动后,就会出现闪退,抓取logcat日志。
因为使用的user版本的手机,所有没有权限读取到/data/tombstones日志,不过logcat日志对于分析本文的问题已经足够;
1 | /* |
2、分析流程
2.1、关于日志
1 | 11-10 22:08:45.968 4449 4449 I crash_dump64: performing dump of process 4418 (target tid = 4418) |
可以看到,日志内容主要由下面几部分组成:(我们最主要的就是分析崩溃的过程和PID,终止的信号和故障地址和调用堆栈部分)
- 构建指纹
- 崩溃的过程和PID
- 终止信号和故障地址
- CPU寄存器
- 调用堆栈
2.1.1、崩溃过程和PID信息
从上面日志中的第6行中我们可以看到崩溃进程的基本信息,如下所示:
1 | pid: 8902, tid: 8902, name: xample.hellojni >>> com.example.hellojni <<< |
如果pid等于tid,那么就说明这个程序是在主线程中Crash掉的,名称的属性则表示Crash进程的名称以及在文件系统中位置。
2.1.2、终止信号和故障地址信息
从上面日志中的第7、8行中我们可以看到程序是因为什么信号导致了Crash以及出现错误的地址,如下所示:
1 | 11-10 19:53:21.890 8926 8926 F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 |
第7行的信息说明出现进程Crash的原因是因为程序产生了段错误的信号,访问了非法的内存空间,而访问的非法地址是0x0。另外这个例子中直接给出来问题原因是因为空指针,其他问题并不一定会给出此信息。
2.1.3、调用堆栈信息
调用栈信息是分析程序崩溃的非常重要的一个信息,它主要记录了程序在Crash前的函数调用关系以及当前正在执行函数的信息,上面例中的backtrace的信息如下所示:
1 | 11-10 22:08:46.305 4449 4449 F DEBUG : backtrace: |
在上面的输出信息中,## 00,#01,#02 ……等表示的都是函数调用栈中栈帧的编号,其中编号越小的栈帧表示着当前最近调用的函数信息,所以栈帧标号#00表示的就是当前正在执行并导致程序崩溃函数的信息。
在栈帧的每一行中,pc后面的16进制数值表示的是当前函数正在执行语句的在共享链接库或者可执行文件中的位置,然后/lib/arm/libhello-jni.so则表示的是当前执行指令是在哪个文件当中,后面的小括号则是注明对应的是哪个函数。
例如,在上面的例子中,我们就可以定位到是程序是在willCrash
中出现了错误,但是具体在那一行呢,我们还不是特别清楚,所以就需要我们进一步地使用更加高级的工具来帮助我们解析日志中有关调用栈的信息。
2.2、addr2line
addr2line是NDK中用来获得指定动态链接库文件或者可执行文件中指定地址对应的源代码信息,它们位于NDK包中的如下位置中,以arm64架构为例:
1 | $$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line |
其中NDK_HOME表示你的NDK的安装路径,另外具体架构和目录的对应关系如下:
工具链 | 位置 |
---|---|
arm | $TOOLCHAIN/arm-linux-androideabi/lib/ |
arm64 | $TOOLCHAIN/aarch64-linux-android/lib/ |
x86 | $TOOLCHAIN/i686-linux-android/lib/ |
x86_64 | $TOOLCHAIN/x86_64-linux-android/lib/ |
addr2line的使用说明如下所示:
1 | Usage: ./aarch64-linux-android-addr2line [option(s)] [addr(s)] |
addr2line的基本用法如下所示:
1 | ./aarch64-linux-android-addr2line -f -e ~/Desktop/workspace/android/min/Demo/HelloJNI/app/build/intermediates/cmake/arm8Debug/obj/arm64-v8a/libhello-jni.so 000000000000065c |
如上所示,通过addr2line工具,我们可以看到libhello-jni.so文件中地址000000000000065c对应的源码是什么了,它对应的是源码中app/src/main/cpp/hello-jni.c:29处代码,查看上下文后,确定为空指针问题。
2.3、ndk-stack
Android NDK自从版本r6开始,提供了一个工具ndk-stack。这个工具能自动分析tombstone文件,能将崩溃时的调用内存地址和c ++代码一行一行对应起来。
ndk-stack工具同样也位于NDK包中,它的路径如下所示:
1 | $NDK_HOME/ndk-stack |
ndk-stack的使用说明如下所示:
1 | Usage: ndk-stack -sym PATH [-dump PATH] |
其中,
- dump参数很容易理解,即dump下来的log文本文件,可以是logcat日志或者tombstones日志;
- sym参数就是你的android项目下,编译成功之后,obj目录下的文件。
ndk-stack的基本用法如下所示:
1 | ndk-stack -sym ~/Desktop/workspace/android/min/Demo/HelloJNI/app/build/intermediates/cmake/arm8Debug/obj/arm64-v8a/ -dump ~/Desktop/min.log |tee ~/Desktop/ndk-stack.txt |
最后产生的结果文件如下,可以看到,堆栈信息的最后对应的是源码中app/src/main/cpp/hello-jni.c:29处代码,查看上下文后,确定为空指针问题。
1 | ********** Crash dump: ********** |
2.4、ndk-stack
上面两种工具都是将崩溃点对应到源码再进行分析,objdump 则是可以在汇编层对崩溃原因进行分析。所以这要求我们必须了解一些 arm/x86 汇编知识。
objdump也是ndk自带的一个工具,通常与addr2line在同一目录:
1 | $$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump |
objdump的使用说明如下所示:
1 | Usage: ./aarch64-linux-android-objdump <option(s)> <file(s)> |
objdump的基本用法如下所示:
1 | objdump ~/Desktop/workspace/android/min/Demo/HelloJNI/app/build/intermediates/cmake/arm8Debug/obj/arm64-v8a/ -dump ~/Desktop/min.log |tee ~/Desktop/ndk-stack.txt |
最后产生的结果文件如下:
1 | 0000000000000648 <willCrash>: |
可以看到,000000000000065c
这个地址的相关两个汇编指令如下:
1 | 658: f94007e9 ldr x9, [sp,#8] |
1、LDR R0, [R1]
LDR是把R1中的值取出放到寄存器R0中LDR:load R0 from register R12、STR R0, [R1]
STR是把R0中的值存入寄存器R1中,STR:store R0 to register R1
结合signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
信息,配合崩溃信号列表:
信号 | 描述 |
---|---|
SIGSEGV | 内存引用无效。 |
SIGBUS | 访问内存对象的未定义部分。 |
SIGFPE | 算术运算错误,除以零。 |
SIGILL | 非法指令,如执行垃圾或特权指令 |
SIGSYS | 糟糕的系统调用 |
SIGXCPU | 超过CPU时间限制。 |
SIGXFSZ | 文件大小限制。 |
我们大体可以猜出来这一个空指针的问题。